using System;
using System.Reflection;
using System.Reflection.Emit;
using System.IO;
using System.Collections;
using System.Collections.Generic;

namespace TodoSoft.Json
{
	/// <summary>
	/// Generate methods which can convert some objects to json-formatted string.
	/// </summary>
	/// <exception cref='Exception'>
	/// Represents errors that occur during application execution.
	/// </exception>
	/// <exception cref='ArgumentNullException'>
	/// Is thrown when an argument passed to a method is invalid because it is <see langword="null" /> .
	/// </exception>
	internal class SerializeIlBuilder: ISerializeTypeBuilder
	{
		private const string ClassName = "Converter";
		private const string MethodName = "Convert";
		private const string Namespace = "TodoSoft.Json.RuntimeCompiled";

		readonly AssemblyBuilder _asmBuilder;
		readonly ModuleBuilder _modBuilder;

		readonly Dictionary<Type, object> _delegates = new Dictionary<Type, object>();
		Type _generatedType;

		protected TypeBuilder TypeBuilder { get; private set; }
		readonly Dictionary<Type, SerializeMethodBuilder> _methodBuilders = new Dictionary<Type, SerializeMethodBuilder>();
		
		public SerializeIlBuilder ()
		{
			// Создаем динамическую сборку.
      		// Указываем в качестве доступа только выполнение.
			var asmName = new AssemblyName {Name = "TodoSoft.Json.RuntimeCompiled"};
			_asmBuilder = AppDomain.CurrentDomain.DefineDynamicAssembly(asmName, AssemblyBuilderAccess.Run);

      		// Создадим модуль. 
      		_modBuilder = _asmBuilder.DefineDynamicModule("main");

      		// Теперь создадим класс сборки
       		TypeBuilder = _modBuilder.DefineType(Namespace + Type.Delimiter + ClassName, TypeAttributes.Public|TypeAttributes.Class);
		}

		protected Type GeneratedType
		{
			get { return _generatedType ?? (_generatedType = TypeBuilder.CreateType()); }
		}
		
		protected SerializeMethodBuilder GetMethodBuilderForType(Type type)
		{
			if(_methodBuilders.ContainsKey(type))
			{
				return _methodBuilders[type];
			}
			
			var builder = new SerializeMethodBuilder(this, type, MethodName);
			_methodBuilders.Add(type, builder);
			return builder;
		}
		
		public void ConfigureConvertionMethod (Type type, Action<ISerializeMethodBuilder> action)
		{
			if(!type.IsPublic)
				throw new Exception("Imposible to serialize not public type: " + type.FullName);
			
			var builder = GetMethodBuilderForType(type);
			
			action(builder);
			
			builder.EndMethod();
		}

		public ConvertDelegate<T> GetConvertionMethod<T>()
		{
			object convertDelegate;
			
			if(!_delegates.ContainsKey(typeof(T)))
			{
				// Извлекаем нужный метод
				var method = GeneratedType.GetMethod(MethodName,  new []{typeof(T), typeof(TextWriter)});
				convertDelegate = Delegate.CreateDelegate(typeof(ConvertDelegate<T>), method);	
				_delegates.Add(typeof(T), convertDelegate);
			}
			else
			{
				convertDelegate = _delegates[typeof(T)];
			}

			return (ConvertDelegate<T>)convertDelegate;
		}


		protected class SerializeMethodBuilder : ISerializeMethodBuilder
		{
			static readonly MethodInfo TextWriterWriteStringMethodInfo;
			static readonly MethodInfo GetEnumeratorMethodInfo;
			static readonly MethodInfo GetCurrentMethodInfo;
			static readonly MethodInfo GetMoveNextMethodInfo;
			static readonly MethodInfo DisposeMethodInfo;

			
			static SerializeMethodBuilder()
			{
				//precached values
				TextWriterWriteStringMethodInfo = typeof(TextWriter).GetMethod("Write",new[]{typeof(string)});
				GetEnumeratorMethodInfo = typeof(IEnumerable).GetMethod("GetEnumerator");
				GetCurrentMethodInfo = typeof(IEnumerator).GetProperty("Current").GetGetMethod();
				GetMoveNextMethodInfo = typeof(IEnumerator).GetMethod("MoveNext");	
				DisposeMethodInfo = typeof(IDisposable).GetMethod("Dispose");
			}
			
			SerializeIlBuilder Owner {get;set;}
			readonly Type _serializeType;
			readonly MethodBuilder _methodBuilder;
			readonly ILGenerator _methodBodyGenerator;
			short _varIndex;
			
			public MethodBuilder MethodBuilder{get {return _methodBuilder;}}
			
			public SerializeMethodBuilder(SerializeIlBuilder owner, Type type, string methodName)
			{
				if (owner == null) throw new ArgumentNullException("owner");
				if (type == null) throw new ArgumentNullException("type");
				
				Owner = owner;
				
				_serializeType = type;
							
				// Define new static method
          		_methodBuilder = owner.TypeBuilder.DefineMethod(
					methodName,	
					MethodAttributes.Public|MethodAttributes.Static, 
					typeof(void), 
					new []{type, typeof(TextWriter)});
				
				// Create IL-Generator for method's body.
          		_methodBodyGenerator = _methodBuilder.GetILGenerator();
			}

			public void EndMethod ()
			{
          		_methodBodyGenerator.Emit(OpCodes.Ret);
			}

			#region ISerializeMethodBuilder implementation

			public void EchoText (string text)
			{
				_methodBodyGenerator.Emit(OpCodes.Ldarg_1);
				_methodBodyGenerator.Emit(OpCodes.Ldstr, text);
				_methodBodyGenerator.EmitCall(OpCodes.Callvirt, TextWriterWriteStringMethodInfo, null);
			}

			public void EchoVar (short varIndex)
			{
				_methodBodyGenerator.Emit(OpCodes.Ldarg_1);
				_methodBodyGenerator.Emit(OpCodes.Ldloc, varIndex);
			
				_methodBodyGenerator.EmitCall(OpCodes.Callvirt, TextWriterWriteStringMethodInfo, null);
			}

		public void Foreach (string name, Action<ISerializeMethodBuilder, short> action)
		{
			var getPropertyMethod = _serializeType.GetProperty(name).GetGetMethod();
			var targetType = getPropertyMethod.ReturnType;
			
			var enumeratorVar = DeclareLocal(GetEnumeratorMethodInfo.ReturnType);
			
			Type itemType = targetType.IsGenericType ? targetType.GetGenericArguments()[0] : typeof(object);
			
			var itemVar = DeclareLocal(itemType);
			
			var startLoop = _methodBodyGenerator.DefineLabel();
			var endLoop = _methodBodyGenerator.DefineLabel();
			var endFinallyMark = _methodBodyGenerator.DefineLabel();
			
			// Get property
			_methodBodyGenerator.Emit(OpCodes.Ldarg_0);
			_methodBodyGenerator.EmitCall(OpCodes.Callvirt, getPropertyMethod, null);
			// Get its IEnumerator
			_methodBodyGenerator.EmitCall(OpCodes.Callvirt, GetEnumeratorMethodInfo, null);
			// Save its
			_methodBodyGenerator.Emit(OpCodes.Stloc, enumeratorVar);
			
			_methodBodyGenerator.BeginExceptionBlock();
			
			_methodBodyGenerator.Emit(OpCodes.Br, endLoop);
			_methodBodyGenerator.MarkLabel(startLoop);
			_methodBodyGenerator.Emit(OpCodes.Ldloc, enumeratorVar);
			_methodBodyGenerator.EmitCall(OpCodes.Callvirt, GetCurrentMethodInfo, null);
			_methodBodyGenerator.Emit(OpCodes.Stloc, itemVar);
			
			action(this, itemVar);
			
			_methodBodyGenerator.MarkLabel(endLoop);
			_methodBodyGenerator.Emit(OpCodes.Ldloc, enumeratorVar);
			_methodBodyGenerator.EmitCall(OpCodes.Callvirt, GetMoveNextMethodInfo, null);
			_methodBodyGenerator.Emit(OpCodes.Brtrue, startLoop);
			
			_methodBodyGenerator.BeginFinallyBlock();
			// Dispose IEnumerator
			_methodBodyGenerator.Emit(OpCodes.Ldloc, enumeratorVar);
			_methodBodyGenerator.Emit(OpCodes.Brfalse, endFinallyMark);
				
			_methodBodyGenerator.Emit(OpCodes.Ldloc, enumeratorVar);
			_methodBodyGenerator.EmitCall(OpCodes.Callvirt, DisposeMethodInfo, null);
				
			_methodBodyGenerator.MarkLabel(endFinallyMark);
			_methodBodyGenerator.EndExceptionBlock();
		}

		public void IfNotNull (string name, Action<ISerializeMethodBuilder> action)
		{
			if (name == null) throw new ArgumentNullException("name");

			var propertyInfo = _serializeType.GetProperty(name);

			var exitLabel = _methodBodyGenerator.DefineLabel();
			_methodBodyGenerator.Emit(OpCodes.Ldarg_0);

			if (propertyInfo != null)
			{
				var getPropertyMethod = propertyInfo.GetGetMethod();
				_methodBodyGenerator.EmitCall(OpCodes.Callvirt, getPropertyMethod, null);
			}
			else
			{
				var fieldInfo = _serializeType.GetField(name);
				if(fieldInfo == null)
					throw new Exception("The type has no field or property named: " + name);
				_methodBodyGenerator.Emit(OpCodes.Ldfld, fieldInfo);
			}

			_methodBodyGenerator.Emit(OpCodes.Brfalse, exitLabel);

			action(this);

			_methodBodyGenerator.MarkLabel(exitLabel);
		}

		public void WriteProperty (string name)
		{
			var property = _serializeType.GetProperty(name);

			if (property != null)
			{
				var getPropertyMethod = property.GetGetMethod();

				var targetType = getPropertyMethod.ReturnType;

				if (targetType.IsEnum)
					targetType = typeof (int);

				var tmpVarIndex = DeclareLocal(targetType);

				_methodBodyGenerator.Emit(OpCodes.Ldarg_1);
				_methodBodyGenerator.Emit(OpCodes.Ldarg_0);

				_methodBodyGenerator.EmitCall(OpCodes.Callvirt, getPropertyMethod, null);

				if (targetType != typeof (string))
				{
					_methodBodyGenerator.Emit(OpCodes.Stloc, tmpVarIndex);
					_methodBodyGenerator.Emit(OpCodes.Ldloca_S, tmpVarIndex);
					var toStringMethod = targetType.GetMethod("ToString", new Type[0]);
					_methodBodyGenerator.EmitCall(OpCodes.Call, toStringMethod, null);
				}

				_methodBodyGenerator.EmitCall(OpCodes.Callvirt, TextWriterWriteStringMethodInfo, null);
			}
			else
			{
				var field = _serializeType.GetField(name);
				if (field == null) return;

				var targetType = field.FieldType;

				if (targetType.IsEnum)
					targetType = typeof(int);

				var tmpVarIndex = DeclareLocal(targetType);

				_methodBodyGenerator.Emit(OpCodes.Ldarg_1);
				_methodBodyGenerator.Emit(OpCodes.Ldarg_0);

				_methodBodyGenerator.Emit(OpCodes.Ldfld, field);

				if (targetType != typeof(string))
				{
					_methodBodyGenerator.Emit(OpCodes.Stloc, tmpVarIndex);
					_methodBodyGenerator.Emit(OpCodes.Ldloca_S, tmpVarIndex);
					var toStringMethod = targetType.GetMethod("ToString", new Type[0]);
					_methodBodyGenerator.EmitCall(OpCodes.Call, toStringMethod, null);
				}

				_methodBodyGenerator.EmitCall(OpCodes.Callvirt, TextWriterWriteStringMethodInfo, null);
			}
		}

		public void CallConvertionMethod (string name)
		{
			if (name == null) throw new ArgumentNullException("name");
			var propertyInfo = _serializeType.GetProperty(name);

			if (propertyInfo != null)
			{
				var getPropertyMethod = propertyInfo.GetGetMethod();

				var convertionMethodInfo = Owner.GetMethodBuilderForType(getPropertyMethod.ReturnType);

				_methodBodyGenerator.Emit(OpCodes.Ldarg_0);
				_methodBodyGenerator.EmitCall(OpCodes.Callvirt, getPropertyMethod, null);
				_methodBodyGenerator.Emit(OpCodes.Ldarg_1);
				_methodBodyGenerator.EmitCall(OpCodes.Call, convertionMethodInfo.MethodBuilder, null);
			}
			else
			{
				var fieldInfo = _serializeType.GetField(name);

				if(fieldInfo == null)
					throw  new Exception("The type have not property or field named: " +name);

				var convertionMethodInfo = Owner.GetMethodBuilderForType(fieldInfo.FieldType);

				_methodBodyGenerator.Emit(OpCodes.Ldarg_0);
				_methodBodyGenerator.Emit(OpCodes.Ldfld, fieldInfo);
				_methodBodyGenerator.Emit(OpCodes.Ldarg_1);
				_methodBodyGenerator.EmitCall(OpCodes.Call, convertionMethodInfo.MethodBuilder, null);
			}
		}
				
		short DeclareLocal(Type type)
		{
			_methodBodyGenerator.DeclareLocal(type);
			
			var num = _varIndex;
			_varIndex++;
			return num;
		}

		public short CreateStringVar (string initValue)
		{
			var num = DeclareLocal(typeof(string));

			if (initValue != null)
				SetVar(num, initValue);

			return num;
		}

		public void SetVar (short varIndex, string varVal)
		{
			_methodBodyGenerator.Emit(OpCodes.Ldstr, varVal);
			_methodBodyGenerator.Emit(OpCodes.Stloc,varIndex);
		}

			#endregion
		}
	}
}